﻿/**
 * IMonAIR: A new open-source library for instant messenger connectivity. <http://code.google.com/p/imonair/>
 *
 * This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
 */
/**
 * @author Andre de Groot <http://www.cmd.tech.nhl.nl/users/groot406/weblog/>
 * @author Piotr Zema <http://felixz.mark-naegeli.com>
 */
package net.imonair.msn.chats {
	import flash.events.EventDispatcher;
	import flash.events.ProgressEvent;
	import flash.events.TimerEvent;
	import flash.events.Event;
	import flash.utils.Timer;
	import flash.text.TextFormat;
	import flash.net.Socket;
	import net.imonair.core.contacts.IIMContact;
	import net.imonair.core.chats.IIMMessage;
	import net.imonair.core.chats.IIMChat;
	import net.imonair.msn.connectors.MSNConnector;
	import net.imonair.msn.contacts.MSNContact;
	import net.imonair.msn.events.MSNChatEvent;
	import net.imonair.msn.utils.serializeTextFormat;
	public class MSNSwitchBoard extends EventDispatcher implements IIMChat {
		private static const LOCAL_MSN_SERVER_IMMITATOR_PORT:uint = 9998;
		private static const LOCAL_MSN_SERVER_IMMITATOR:String = "localhost";
		private static const USE_LOCAL_SERVER:Boolean = false;
		private static const CONTENT_TYPE_MESSAGE		:String = "text/plain";
		private static const CONTENT_TYPE_NOTIFICATION	:String = "text/x-msmsgscontrol";
		private static const CONTENT_TYPE_INVITATION	:String = "text/x-msmsgsinvite";
		private static const CONTENT_TYPE_FILE_TRANSFER	:String = "application/x-msnmsgrp2p";
		private var host:String, userLiveID:String, _sessionID:String, ticket:String, type:String;
		private var currentContacts:Array = [];
		private var connector:MSNConnector;
		private var port:uint, _trID:uint;
		private var debug:Boolean = false;
		private var keepAliveTimer:Timer;
		private var socket:Socket;
		private var _connectTrID:uint = 0;
		
		public function MSNSwitchBoard(connector:MSNConnector, host:String, port:uint, userLiveID:String, sessionID:String, ticket:String, type:String = 'ANSWER', trID:uint = 0, debug:Boolean = true) {
			this.connector = connector;
			this.host = host;
			this.port = port;
			this.userLiveID = userLiveID;
			this._sessionID = sessionID;
			this.ticket = ticket;
			this.type = type;
			this._trID = trID;
			this.debug = debug;
			if (USE_LOCAL_SERVER){ 
				this.socket = new Socket(LOCAL_MSN_SERVER_IMMITATOR, LOCAL_MSN_SERVER_IMMITATOR_PORT);
				this.keepAliveTimer = new Timer(100);
				this.keepAliveTimer.addEventListener(TimerEvent.TIMER, keepAliveListener);
				this.keepAliveTimer.start();
			} else this.socket = new Socket(this.host, this.port);
			this.socket.addEventListener(Event.CONNECT, connectHandler);
			this.socket.addEventListener(Event.CLOSE, closeHandler);
			this.socket.addEventListener(ProgressEvent.SOCKET_DATA, dataHandler);
		}
		/**
		 * Returns an array which contains all currently participating contacts.
		 */
		public function get contacts():Array {
			return this.currentContacts;
		}
		/**
		 * @private
		 */
		public function get sessionID():String {
			return this._sessionID;
		}
		
		public function get connectTrID():uint {
			return this._connectTrID;
		}
		
/*		public function get trID():uint	{
			return this._trID;
		}
*/		public function sendMessage(value:IIMMessage):void {//TO DO: finish textFormat handling
			var message:String = prepareMessage(value.message);
			var X_MMS_IM:String = serializeTextFormat(value.textFormat);
			this.sendCommand("MSG " + this._trID + " A " + (message.length+2) + "\r\n\r\n" + message);
			this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.MESSAGE_SENT));
		}
		/**
		 * Returns a contact currently participating in conversation by its LiveID.
		 * @param id The LiveID of the contact
		 * @return A MSNContact or <code>null</code> when contact not found.
		 */
		public function getContactByID(id:String):MSNContact {
			for each (var contact:MSNContact in this.currentContacts) if (contact.id == id) return contact;
			return null;
		}
		/**
		 * Adds a new contact into conversation.
		 * @param contact A MSNContact instance to be added.
		 */
		public function addContact(contact:IIMContact):void {
			for each (var currentContact:MSNContact in this.currentContacts) if (currentContact.id == contact.id) return;
			this.sendCommand("CAL " + this._trID + " " + contact.id + "\r\n");
		}
		/**
		 * Closes connection.
		 */
		public function close():void {
			sendCommand("OUT\r\n");
		}
		private function _addContactByID(id:String, displayName:String = ""):Boolean {
			var contact:MSNContact = connector.getContact(id) as MSNContact;
			if (!contact) {
				contact = new MSNContact(connector);
				contact.id = id;
				contact.displayName = displayName || id;
			}
			return _addContact(contact);
		}
		private function _addContact(value:MSNContact):Boolean {
			if (getContactByID(value.id)) return false;
			this.currentContacts.push(value);
			if (this.currentContacts.length > 1) {
				for each (var contact:MSNContact in this.currentContacts) contact.closeSwitchBoard(false);
				this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.CONTACT_JOINED));
			} else {
				this.currentContacts[0].switchBoard = this;
				this.dispatchEvent(new MSNChatEvent(MSNChatEvent.START));			
			}
			return true;
		}
		private function _removeContact(value:MSNContact):Boolean {
			return _removeContactByID(value.id);
		}
		private function _removeContactByID(value:String):Boolean {
			for (var i:uint = 0; i < this.currentContacts.length; i++)	{
				if (this.currentContacts[i].id == value) {
					this.currentContacts.splice(i, 1);
					this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.CONTACT_LEFT));
					return true;
				}
			}
			return false;
		}
		private function sendCommand(value:String):void {
			this.socket.writeUTFBytes(value);
			this.socket.flush();
			if (value != "KAL") {
				this._trID++;
				debugFunction(">>> " + value.substr(0,-2));
			}
		}
		private function keepAliveListener(event:TimerEvent):void {
			sendCommand("KAL");
		}
		private function prepareMessage(message:String):String {
			return "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nX-MMS-IM-Format: FN=Arial; EF=I; CO=0; CS=0; PF=22\r\n\r\n" + message;
		}
		private function connectHandler(event:Event):void {
			debugFunction('Connected to switchboard.');
			if (USE_LOCAL_SERVER) sendCommand('CON ' + this.host + ':' + this.port + '\r\n') else {
				if (this.type == "ANSWER") sendCommand("ANS " + this._trID + " " + this.userLiveID + " " + this.ticket + " " + this.sessionID + "\r\n");
				if (this.type == "CALL") sendCommand("USR " + this._trID + " " + this.userLiveID + " " + this.ticket + "\r\n");
			} 
		}
		private function closeHandler(event:Event):void {
			debugFunction('Connection to switchboard terminated.');
		}
		private function dataHandler(event:ProgressEvent):void {
			var value:String = readData();
//			for each (var value:String in responseArray) try {
			try {
				this["handleDataCode_" + value.substr(0, 3)](value);
			} catch (error:Error) {
				debugFunction("Response not supported: " + value);
			}
		}
		private function handleDataCode_CON(value:String):void {// The local server connected succesfull to the given server
			if (this.type == "ANSWER") sendCommand("ANS " + this._trID + " " + this.userLiveID + " " + this.ticket + " " + this.sessionID + "\r\n");
			if (this.type == "CALL") sendCommand("USR " + this._trID + " " + this.userLiveID + " " + this.ticket + "\r\n");
		}
		private function handleDataCode_ANS(value:String):void {//Your answer is received by the switchboard server
			this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.MESSAGE_SENT));
		}
		private function handleDataCode_ACK(value:String):void {//Message successfully received by every participant
			this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.MESSAGE_DELIVERED));			
		}
		private function handleDataCode_NAK(value:String):void {//one or more participants did not successfully receive user's message
			this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.MESSAGE_FAILED));			
		}
		private function handleDataCode_MSG(value:String):void {//You've received a message//---------------------------------
			var data:Array = value.split("\r\n");
			var sender:IIMContact = getContactByID(value.split(" ")[1]);
			var params:Object = {};
			var index:uint = 1;
			for each (var line:String in data) if (line) {
				var param:Array = line.split(": ");
				params[param[0]] = param[1];
				index += 1;
			} else break;
			switch(params["Content-Type"]) {
				case CONTENT_TYPE_MESSAGE:
				case CONTENT_TYPE_MESSAGE + "; charset=UTF-8":
					var textFormat:TextFormat = new TextFormat();
					var message:MSNMessage = new MSNMessage(data.slice(index).join("\n"), textFormat);
					message.sender = sender;
					this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.MESSAGE_RECEIVED, message));
					break;
				case CONTENT_TYPE_NOTIFICATION:
				case CONTENT_TYPE_NOTIFICATION + "; charset=UTF-8":
					var typingMessage:MSNMessage = new MSNMessage("");
					typingMessage.sender = sender;
					this.connector.dispatchEvent(new MSNChatEvent(MSNChatEvent.USER_IS_TYPING, typingMessage));
					break;
				case CONTENT_TYPE_INVITATION:
				case CONTENT_TYPE_INVITATION + "; charset=UTF-8":
					debugFunction(value);
					break;
				case CONTENT_TYPE_FILE_TRANSFER:
					debugFunction(value);
					break;
				default:
					debugFunction("Unknown content type: " + params["Content-Type"]);
			}
		}
		private function handleDataCode_USR(value:String):void {
			var event:MSNChatEvent = new MSNChatEvent(MSNChatEvent.CONNECT);
			event.switchBoard = this;
			this._connectTrID = value.split(" ")[1];
			this.connector.dispatchEvent(event);
		}
		private function handleDataCode_CAL(value:String):void {
			this._sessionID = value.split(" ")[3];
		}
		private function handleDataCode_JOI(value:String):void {
			var data:Array = value.split(" ");
			_addContactByID(data[1], data[2]);
		}
		private function handleDataCode_IRO(value:String):void {
			var data:Array = value.split(" ");
			_addContactByID(data[4], data[5]);
		}
		private function handleDataCode_BYE(value:String):void {
			var id:String = value.split(" ")[1];
			var contact:MSNContact = this.connector.getContact(id) as MSNContact;
			if (contact) contact.closeSwitchBoard();
			_removeContactByID(id);
		}
		private function readData():String {
			var response:String = this.socket.readUTFBytes(socket.bytesAvailable);
			debugFunction("<<< " + response);
			return response;
		}
		private function debugFunction(value:String):void {
			if (this.debug) trace(value);
		}
	}
}